package com.lining.orangeservice.aspect;

import com.lining.orangeservice.entity.po.ApiLog;
import com.lining.orangeservice.entity.po.User;
import com.lining.orangeservice.service.ApiLogService;
import com.lining.orangeservice.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;

/**
 * API调用日志记录切面类
 * <p>
 * 该切面专门用于拦截和记录所有控制器层方法的调用情况，提供完整的API访问日志功能。
 * 主要功能包括：
 * 1. 记录API调用的详细信息，如请求URL、方法类型、参数等
 * 2. 统计API执行时间，用于性能监控和分析
 * 3. 记录调用者信息（用户ID、用户名），便于追踪操作来源
 * 4. 记录客户端信息（IP地址、User-Agent），用于安全审计
 * 5. 捕获并记录异常信息，便于问题排查
 * 6. 异步写入日志，避免影响主业务流程性能
 * </p>
 * <p>
 * 设计原则：
 * - 非侵入性：通过AOP实现，无需修改原有业务代码
 * - 性能友好：采用异步方式写入日志，减少对主流程的影响
 * - 完整性：记录调用过程中的所有重要信息
 * - 容错性：即使日志记录失败也不影响主业务流程
 * </p>
 * <p>
 * 注意事项：
 * - 该切面仅适用于Spring管理的Bean
 * - 需要确保ApiLogService和AuthService能够正常注入
 * - 异步日志记录依赖于Spring的TaskExecutor，默认使用SimpleAsyncTaskExecutor
 * </p>
 */
@Aspect
@Component
@Slf4j
public class ApiLogAspect {
    
    /**
     * API日志服务，负责将日志信息持久化到数据库
     * <p>
     * 通过该服务将收集到的API调用信息保存到sys_api_log表中，
     * 便于后续的查询、统计和分析操作。
     * </p>
     */
    @Autowired
    private ApiLogService apiLogService;
    
    /**
     * 认证服务，用于获取当前登录用户信息
     * <p>
     * 通过该服务可以从SecurityContext或Session中获取当前登录用户的信息，
     * 用于记录API调用的操作者身份。
     * </p>
     */
    @Autowired
    private AuthService authService;
    
    /**
     * 定义切点，匹配controller包下所有类的所有方法
     * <p>
     * 切点表达式详细说明:
     * execution(* com.lining.orangeservice.controller..*.*(..))
     * - execution: 表示匹配方法执行连接点，是最常用的切点指示符
     * - 第一个*: 匹配任意返回值类型
     * - com.lining.orangeservice.controller: 指定包路径
     * - ..: 表示当前包及其所有子包
     * - 第二个*: 匹配包内的任意类
     * - .*: 匹配类中的任意方法
     * - (..): 匹配任意参数列表（0个或多个参数）
     * </p>
     * <p>
     * 为什么选择这种方式？
     * 1. 统一管理：集中记录所有控制器接口的调用情况
     * 2. 全面覆盖：避免遗漏重要的API接口
     * 3. 易于维护：新增控制器接口时无需额外配置
     * </p>
     * <p>
     * 可能的替代方案及对比：
     * 1. 基于注解的方式：需要在每个需要记录日志的方法上添加注解，灵活性高但维护成本大
     * 2. 基于包名精确匹配：可以精确控制记录哪些包下的接口，但需要预先知道所有包名
     * 3. 基于正则表达式：可以实现复杂的匹配规则，但表达式编写和维护较复杂
     * 当前选择的方案在全面性和易维护性之间取得了较好的平衡。
     * </p>
     */
    @Pointcut("execution(* com.lining.orangeservice.controller..*.*(..))")
    public void controllerMethods() {}
    
    /**
     * 环绕通知，记录API调用日志的核心实现
     * <p>
     * 该方法是整个日志记录功能的核心，其执行流程如下：
     * 1. 记录方法开始执行时间，用于后续计算执行耗时
     * 2. 从当前线程中获取HTTP请求对象及相关信息
     * 3. 构造API日志对象，并填充基本请求信息
     * 4. 尝试获取当前登录用户信息并设置到日志对象中
     * 5. 记录方法调用参数
     * 6. 执行目标方法并捕获返回结果或异常
     * 7. 根据执行结果设置响应信息和状态码
     * 8. 异步保存日志到数据库
     * </p>
     * <p>
     * 异常处理策略：
     * - 正常执行流程：记录成功状态和返回结果
     * - 发生业务异常：记录异常状态和异常信息，但仍抛出异常以保持原有处理逻辑
     * - 获取用户信息异常：仅记录调试日志，不影响主流程
     * - 日志保存异常：由于采用异步方式，即使保存失败也不会影响主流程
     * </p>
     * <p>
     * 性能优化措施：
     * 1. 异步写入：使用CompletableFuture异步执行日志保存操作
     * 2. 时间计算：精确到毫秒级的时间统计
     * 3. 参数序列化：使用Arrays.toString()简化参数输出
     * 4. 异常隔离：确保日志记录异常不会影响主业务流程
     * </p>
     *
     * @param joinPoint 连接点对象，封装了目标方法的相关信息
     *                  包括：目标对象、方法签名、参数列表等
     * @return 目标方法的原始返回值，确保不影响原有业务逻辑
     * @throws Throwable 目标方法可能抛出的任何异常，确保异常传播不受影响
     */
    @Around("controllerMethods()")
    public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        // 记录方法开始执行时间（单位：毫秒），用于计算方法执行耗时
        // System.currentTimeMillis()返回自1970年1月1日00:00:00 UTC以来的毫秒数
        long startTime = System.currentTimeMillis();
        
        // 获取当前HTTP请求对象，用于提取请求相关信息
        // 通过Spring的RequestContextHolder从当前线程中获取请求上下文
        HttpServletRequest request = getCurrentRequest();
        
        // 获取被调用方法的名称，例如"login"、"register"等
        // getSignature()返回方法签名对象，getName()获取方法名
        String methodName = joinPoint.getSignature().getName();
        
        // 获取被调用方法所在类的简单名称，例如"AuthController"、"UserController"等
        // getClass().getSimpleName()返回不带包名的类名
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        // 创建API日志对象，用于存储本次调用的相关信息
        // ApiLog是一个POJO类，对应数据库中的sys_api_log表
        ApiLog apiLog = new ApiLog();
        
        // 设置API名称，格式为 类名.方法名，例如"AuthController.login"
        // 这种命名方式便于快速识别是哪个控制器的哪个方法
        apiLog.setApiName(className + "." + methodName);
        
        // 设置请求URL路径，例如"/auth/login"、"/user/list"等
        // getRequestURI()返回请求的完整路径（不包括协议、主机和端口部分）
        apiLog.setRequestUrl(request.getRequestURI());
        
        // 设置HTTP请求方法（GET/POST/PUT/DELETE等）
        // getMethod()返回HTTP方法字符串，如"GET"、"POST"等
        apiLog.setRequestMethod(request.getMethod());
        
        // 设置客户端IP地址，用于安全审计和访问统计
        // 通过专用方法获取真实客户端IP（考虑代理情况）
        apiLog.setIpAddress(getClientIpAddress(request));
        
        // 设置用户代理信息（浏览器等客户端信息），例如：
        // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        // 用于分析客户端类型和兼容性测试
        apiLog.setUserAgent(request.getHeader("User-Agent"));
        
        // 尝试获取当前登录用户信息，用于追踪操作者身份
        // 使用try-catch块确保即使获取用户信息失败也不会影响主流程
        try {
            // 从认证服务中获取当前登录用户
            // getCurrentUser()方法会从SecurityContext或Session中获取用户信息
            User currentUser = authService.getCurrentUser();
            
            // 如果用户已登录（currentUser不为null），则记录用户ID和用户名
            // 这些信息对于安全审计和操作追踪非常重要
            if (currentUser != null) {
                // 设置用户ID，用于关联用户表进行详细查询
                apiLog.setUserId(currentUser.getId());
                
                // 设置用户名，便于直观查看操作者
                apiLog.setUsername(currentUser.getUsername());
            }
            // 如果currentUser为null，表示用户未登录，这是正常情况（如登录接口本身）
            // 此时不设置用户信息，日志中对应的user_id和username字段将为null
        } catch (Exception e) {
            // 如果获取用户信息过程中发生异常（如认证服务不可用），仅记录调试日志
            // 不影响主流程，因为用户信息不是必须的（某些接口可能确实不需要登录）
            log.debug("无法获取当前用户信息: {}", e.getMessage());
        }
        
        // 记录请求参数，将参数数组转换为字符串形式存储
        // getArgs()返回方法调用时传入的实际参数数组
        // Arrays.toString()将数组转换为"[arg1, arg2, ...]"格式的字符串
        // 注意：对于包含敏感信息的对象，可能需要特殊处理以避免泄露密码等信息
        Object[] args = joinPoint.getArgs();
        apiLog.setRequestParams(Arrays.toString(args));
        
        // 使用try-catch-finally结构确保无论方法执行成功与否都能正确处理日志
        try {
            // 执行目标方法，获取返回结果
            // proceed()方法会执行被拦截的方法，并返回其结果
            // 如果目标方法抛出异常，该异常会被原样抛出并在catch块中处理
            Object result = joinPoint.proceed();
            
            // 计算方法执行耗时（毫秒）
            // 通过结束时间减去开始时间得到准确的执行时间
            long executeTime = System.currentTimeMillis() - startTime;
            
            // 设置执行耗时，用于性能监控和分析
            apiLog.setExecuteTime(executeTime);
            
            // 设置响应状态码，正常执行为200（HTTP OK）
            // 这里使用200表示业务逻辑执行成功，而非HTTP状态码
            apiLog.setStatusCode(200);
            
            // 设置响应结果，如果结果不为空则转换为字符串存储
            // toString()方法会调用对象的toString实现，对于Result对象通常会输出JSON格式
            // 如果result为null（void方法等情况），则存储空字符串
            apiLog.setResponseResult(result != null ? result.toString() : "");
            
            // 返回目标方法的执行结果，确保不影响原有业务逻辑
            return result;
        } catch (Exception e) {
            // 如果目标方法执行异常，记录异常信息
            // 这里捕获的是目标方法抛出的任何异常
            
            // 计算方法执行耗时（包括异常处理时间）
            // 即使发生异常，也需要记录执行时间用于性能分析
            long executeTime = System.currentTimeMillis() - startTime;
            
            // 设置执行耗时
            apiLog.setExecuteTime(executeTime);
            
            // 设置响应状态码为500（服务器内部错误）
            // 表示业务逻辑执行过程中发生了异常
            apiLog.setStatusCode(500);
            
            // 设置异常信息，便于问题排查
            // 格式为"Error: "加上异常消息，便于在日志中识别
            apiLog.setResponseResult("Error: " + e.getMessage());
            
            // 重新抛出异常，确保原有异常处理逻辑不受影响
            // 这样控制器层的异常处理器仍能正常工作
            throw e;
        } finally {
            // 无论方法执行成功与否，都在finally块中异步保存API调用日志
            // finally块确保日志一定会被记录，即使在try或catch块中有return语句
            
            // 使用CompletableFuture实现异步保存，避免影响主业务流程的响应时间
            // runAsync()方法会使用默认的ForkJoinPool执行任务
            // 这意味着日志保存操作将在另一个线程中执行，不会阻塞当前请求线程
            CompletableFuture.runAsync(() -> apiLogService.saveApiLog(apiLog));
            
            // 注意：这里没有等待异步任务完成，这样可以最大化性能
            // 但也意味着如果保存过程中出现异常，主线程无法感知
            // 在实际生产环境中，可能需要更完善的异常处理机制
        }
    }
    
    /**
     * 获取当前HTTP请求对象的私有辅助方法
     * <p>
     * 该方法利用Spring的RequestContextHolder从当前线程中获取绑定的HTTP请求对象。
     * 在Spring MVC应用中，DispatcherServlet会在处理请求前将HttpServletRequest对象
     * 绑定到当前线程的ThreadLocal变量中，使得在整个请求处理过程中都可以方便地获取。
     * </p>
     * <p>
     * 实现原理：
     * 1. RequestContextHolder使用ThreadLocal存储请求上下文
     * 2. DispatcherServlet在处理请求前会调用RequestContextHolder.setRequestAttributes()
     * 3. 在请求处理完成后会清除ThreadLocal中的数据
     * </p>
     * <p>
     * 返回值说明：
     * - 正常情况下返回当前请求的HttpServletRequest对象
     * - 如果不在Web请求上下文中（如定时任务）则返回null
     * - 如果请求上下文类型不匹配也返回null
     * </p>
     *
     * @return 当前HTTP请求对象，如果不存在或类型不匹配则返回null
     */
    private HttpServletRequest getCurrentRequest() {
        // 获取当前线程绑定的请求属性
        // RequestContextHolder.getRequestAttributes()是一个静态方法
        // 它会从ThreadLocal变量中获取当前线程绑定的RequestAttributes对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        
        // 判断获取到的请求属性是否为ServletRequestAttributes类型
        // ServletRequestAttributes是RequestAttributes的一个实现类
        // 它包装了HttpServletRequest和HttpServletResponse对象
        if (requestAttributes instanceof ServletRequestAttributes) {
            // 转换为ServletRequestAttributes并返回其中的HttpServletRequest对象
            // getRequest()方法返回当前的HttpServletRequest实例
            return ((ServletRequestAttributes) requestAttributes).getRequest();
        }
        
        // 如果没有绑定请求（requestAttributes为null）或者
        // 请求属性类型不匹配（不是ServletRequestAttributes），返回null
        // 这种情况可能发生在非Web请求环境中（如定时任务、消息队列处理等）
        return null;
    }
    
    /**
     * 获取客户端真实IP地址的私有辅助方法
     * <p>
     * 在实际部署环境中，应用往往位于反向代理（如Nginx、Apache）之后，
     * 直接通过request.getRemoteAddr()只能获取到代理服务器的IP地址，
     * 而无法获取到真实的客户端IP。因此需要检查HTTP头部信息来获取真实IP。
     * </p>
     * <p>
     * IP地址获取优先级：
     * 1. X-Forwarded-For: 最常用的标准头部，可能包含多个IP地址
     * 2. X-Real-IP: Nginx等代理常用的头部
     * 3. getRemoteAddr(): 直连客户端的IP地址
     * </p>
     * <p>
     * 安全注意事项：
     * - HTTP头部可以被伪造，不应完全信任
     * - 在安全性要求极高的场景中，应结合网络设备日志进行验证
     * - 应在代理服务器上配置正确的头部处理规则
     * </p>
     *
     * @param request HTTP请求对象，不能为空
     * @return 客户端真实IP地址，如果无法确定则返回"unknown"
     */
    private String getClientIpAddress(HttpServletRequest request) {
        // 尝试从X-Forwarded-For头获取IP地址
        // X-Forwarded-For是事实上的标准，格式为"client, proxy1, proxy2"
        // 其中第一个IP是真实客户端IP
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        
        // 检查X-Forwarded-For是否存在且非空
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            // X-Forwarded-For可能包含多个IP地址，用逗号分隔
            // 第一个为真实客户端IP，其余为经过的代理服务器IP
            // split(",")[0]获取第一个IP，trim()去除可能的空格
            return xForwardedFor.split(",")[0].trim();
        }
        
        // 如果X-Forwarded-For不存在，尝试从X-Real-IP头获取IP地址
        // X-Real-IP是Nginx等代理服务器常用的头部
        String xRealIp = request.getHeader("X-Real-IP");
        
        // 检查X-Real-IP是否存在且非空
        if (xRealIp != null && !xRealIp.isEmpty()) {
            // 直接返回X-Real-IP的值
            return xRealIp;
        }
        
        // 如果以上头部都不存在，则使用直接连接的客户端IP地址
        // getRemoteAddr()返回发送请求的客户端IP地址
        // 在无代理的情况下是真实客户端IP，在有代理情况下是最后一个代理的IP
        return request.getRemoteAddr();
    }
}